/**************************************************************************\
    Copyright Microsoft Corporation. All Rights Reserved.
\**************************************************************************/

namespace C4F.DevKit.Contacts
{
    using C4F.DevKit.Contacts.Interop;
    using Microsoft.Win32;
    using C4F.DevKit.Contacts.Standard;
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.IO;
    using System.Threading;
    using System.Windows.Threading;
    using System.Diagnostics.CodeAnalysis;
    using C4F.DevKit.Contacts.Data;
    using C4F.DevKit.Contacts.Data.SimpleContactDataSetTableAdapters;

    /// <summary>
    /// Manages the user's address book.
    /// </summary>
    /// <remarks>
    /// This class provides basic management functionality for the user's contacts.
    /// Contacts can be loaded by their ContactId string, or the entire address book
    /// can be enumerated.
    /// </remarks>
    public class ContactManager : DispatcherObject, IDisposable
    {

        #region Fields
        private const string _exceptionStringBadThreadId = "Contacts can only be created on STA threads and can only be accessed from the thread on which they were created.";
        private const string _meRegKey = @"HKEY_CURRENT_USER\Software\Microsoft\WAB\Me";

        private string _rootDirectory;
        private string _meRegValue;
        private ContactWatcher _watcher;
        private bool _disposed;
        #endregion

        #region Private Utilities

        private string _GetMeContactId()
        {
            return Registry.GetValue(_meRegKey, _meRegValue, null) as string;
        }

        /// <summary>
        /// Is the path considered to be part of the user's address book?
        /// </summary>
        /// <param name="path">The path to check for containment.  This can be a directory or a file name.</param>
        /// <returns>Whether this path is part of the user's address book.</returns>
        /// <remarks>
        /// This does not check for whether the path exists.
        /// </remarks>
        private bool _IsContainedPath(string path)
        {
            Assert.IsTrue(CheckAccess());
            if (!string.IsNullOrEmpty(path))
            {
                return path.StartsWith(RootDirectory);
            }
            return false;
        }

        /// <summary>
        /// Proxy for the CollectionChanged event that hides issues with re-entrancy and no listeners.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        /// <param name="e">Event arguments.</param>
        internal void _NotifyCollectionChanged(object sender, ContactCollectionChangedEventArgs e)
        {
            Assert.IsTrue(CheckAccess());

            // By assigning to a temporary we avoid race conditions with the event losing listeners
            // after the null check, and ensure that we don't end up in any kind of weird cycle due
            // to re-entrancy.
            ContactCollectionChangedEventHandler handler = _CollectionChangedInternal;
            if (null != handler)
            {
                handler(sender, e);
            }
        }

        private void _SetMeContactId(string id)
        {
            Validate.NeitherNullNorEmpty(id, "id");
            Registry.SetValue(_meRegKey, _meRegValue, id, RegistryValueKind.String);
        }

        /// <summary>Try to create a contact from a file.  Swallow reasonable exceptions.</summary>
        /// <param name="filePath">The file to load from</param>
        /// <param name="contact">The contact to return</param>
        /// <returns></returns>
        private static bool _TryLoadContactFromFile(string filePath, out Contact contact)
        {
            contact = null;
            try
            {
                contact = new Contact(filePath);
                return true;
            }
            catch (FileNotFoundException) { }
            catch (InvalidDataException) { }
            catch (UnauthorizedAccessException) { }

            return false;
        }

        /// <summary>
        /// Perform basic validation to ensure that this object is in a valid state.
        /// This should be called at the beginning of any external-facing method.
        /// </summary>
        private void _Validate()
        {
            // DispatcherObject baseclass enforces that this object
            // is only accessed from the thread on which it was created.
            VerifyAccess();
            // All the constructors should catch this also.
            Assert.IsApartmentState(ApartmentState.STA);

            if (_disposed)
            {
                throw new ObjectDisposedException("this");
            }
        }
        #endregion

        #region Constructors

        /// <summary>
        /// Create a new ContactManager.
        /// </summary>
        /// <remarks>
        /// This constructor roots the manager in the user's Contacts folder.
        /// </remarks>
        public ContactManager()
            : this("*")
        {
        }

        /// <summary>
        /// Create a new ContactManager rooted in a specific directory.
        /// </summary>
        /// <param name="rootDirectory">
        /// The directory to use as the root.  Use "*" for the user's Contacts folder.
        /// Use a path prefixed with "*", e.g. "*\subfolder" to user a root directory relative
        /// to the user's Contacts folder.
        /// </param>
        public ContactManager(string rootDirectory)
        {
            Validate.IsApartmentState(ApartmentState.STA, _exceptionStringBadThreadId);
            Validate.NeitherNullNorEmpty(rootDirectory, "rootDirectory");

            if (rootDirectory.StartsWith("*"))
            {
                rootDirectory = Path.Combine(ContactUtil.GetContactsFolder(), rootDirectory.Substring(1).TrimStart('\\', '/'));
                Assert.IsTrue(Path.IsPathRooted(rootDirectory));
            }

            // This might throw, but I want that exception to propagate out.
            _rootDirectory = Path.GetFullPath(rootDirectory);

            // If this is the same as the user's contacts folder then we can share the Me contact.
            // Otherwise use a different registry value.
            //
            // This has weird implications if the Contacts folder is redirected while the manager
            // is up and running.  The behavior there is intentional, as that's very much an edge case.
            _meRegValue = _rootDirectory.Equals(ContactUtil.GetContactsFolder())
                ? ""
                : _rootDirectory;

            if (!Directory.Exists(_rootDirectory))
            {
                Directory.CreateDirectory(_rootDirectory);
            }

            // Delay initializing the _watcher until someone's actually listening for changes.
            // It's fairly expensive, no need to do it if the caller doesn't care.
        }

        #endregion

        #region Public Properties and Methods

        /// <summary>
        /// Load a Contact from a contactId string.
        /// </summary>
        /// <param name="contactId">The identifier for the contact to load.</param>
        /// <returns>
        /// The contact that maps to the provided identifier.
        /// </returns>
        /// <remarks>
        /// The full Contact ID string is a composite of different ways to identify a contact.
        /// All contacts contain a heirarchical "ContactID" property as a Guid.  Contacts
        /// that are backed by a file may also contain the path as part of the ID.
        /// These IDs can be retrieved from Contact's Id property.  Passing that Id to GetContact
        /// or TryGetContact will resolve to a Contact.  Usually it will resolve to the same contact
        /// as the one where the Id was gotten even if the contact has been updated, or the
        /// backing file has been renamed, copied, or moved.  If the contact has been deleted
        /// then this will try to find a compatible contact to load (e.g. if the file was copied
        /// before being deleted, it may find the copy), but if no contact can be found with
        /// the identifier then GetContact will throw an UnreachableContactException (whereas TryGetContact
        /// will simply return false).  If two contacts have been merged, then calling GetContact on
        /// the Id of one of the original contacts will generally load the merged one.<para/>
        /// This will not load contacts that are outside the user's address book.
        /// </remarks>
        /// <exception cref="UnreachableContactException">
        /// If the contactId doesn't resolve to a contact in the user's address book.
        /// </exception>
        public Contact GetContact(string contactId)
        {
            _Validate();
            Validate.NeitherNullNorEmpty(contactId, "contactId");

            Contact contact = null;
            if (!TryGetContact(contactId, out contact))
            {
                throw new UnreachableContactException("The contact couldn't be loaded by this manager.");
            }
            return contact;
        }

        /// <summary>
        /// Try to load a Contact from a contactId string.
        /// This returns a Boolean on failure rather than throw an exception for common errors.
        /// </summary>
        /// <param name="contactId">
        /// The identifier for the contact to load.
        /// </param>
        /// <param name="contact">
        /// When true is returned, this contains the contact that maps to the provided identifier.
        /// If false is returned this parameter is set to null.
        /// </param>
        /// <returns>
        /// Returns true if a contact could be found with the provided identifier.
        /// </returns>
        /// <remarks>
        /// The full Contact ID string is a composite of different ways to identify a contact.
        /// All contacts contain a heirarchical "ContactID" property as a Guid.  Contacts
        /// that are backed by a file may also contain the path as part of the ID.
        /// These IDs can be retrieved from Contact's Id property.  Passing that Id to GetContact
        /// or TryGetContact will resolve to a Contact.  Usually it will resolve to the same contact
        /// as the one where the Id was gotten even if the contact has been updated, or the
        /// backing file has been renamed, copied, or moved.  If the contact has been deleted
        /// then this will try to find a compatible contact to load (e.g. if the file was copied
        /// before being deleted, it may find the copy), but if no contact can be found with
        /// the identifier then GetContact will throw an UnreachableContactException (whereas TryGetContact
        /// will simply return false).  If two contacts have been merged, then calling GetContact on
        /// the Id of one of the original contacts will generally load the merged one.<para/>
        /// This will not load contacts that are outside the user's address book.
        /// </remarks>
        public bool TryGetContact(string contactId, out Contact contact)
        {
            contact = null;

            _Validate();
            Validate.NeitherNullNorEmpty(contactId, "contactId");

            Contact maybeContact = null;

            string path = ContactUtil.TokenizeContactId(contactId, ContactIdToken.Path);
            string guidString = ContactUtil.TokenizeContactId(contactId, ContactIdToken.Guid);
            string fileName = Path.GetFileNameWithoutExtension(path);

            Assert.IsNotNull(guidString);

            Guid id;
            if (!Utility.GuidTryParse(guidString, out id))
            {
                return false;
            }

            // If there's a file part then try to use it.
            if (_IsContainedPath(path))
            {
                // Could fail for a bunch of reasons almost all of which are ignoreable.
                if (_TryLoadContactFromFile(path, out maybeContact))
                {
                    if (maybeContact.ContactIds.Contains(id))
                    {
                        contact = maybeContact;
                        maybeContact = null;
                        return true;
                    }
                    maybeContact.Dispose();
                }
            }

            Assert.IsNull(maybeContact);
            try
            {
                foreach (Contact c in this.GetContactCollection())
                {
                    if (c.ContactIds.Contains(id))
                    {
                        if (Path.GetFileNameWithoutExtension(c.Path).Equals(fileName))
                        {
                            contact = c;
                            return true;
                        }
                        else if (null == maybeContact)
                        {
                            // Keep going in case there's a better match.
                            maybeContact = c;
                            // skip the Dispose at the end of this.
                            continue;
                        }
                    }
                    c.Dispose();
                }

                if (null != maybeContact)
                {
                    contact = maybeContact;
                    maybeContact = null;
                    return true;
                }
            }
            finally
            {
                Utility.SafeDispose(ref maybeContact);
            }

            // Still no contact :(
            return false;
        }

        /// <summary>
        /// Create a new Contact associated with this manager.
        /// </summary>
        /// <returns>
        /// Returns a new Contact associated with this manager.
        /// </returns>
        /// <remarks>
        /// The ContactType of the created contact is ContactType.Contact.<para/>
        /// This is similar to creating a contact with Contact's default constructor, except that CommitChanges
        /// will work despite that there is no file initially backing the contact.  After CommitChanges is
        /// called on the contact can later be loaded by Id with this manager.<para/>
        /// The contact is not considered to be part of this manager until it has been committed.
        /// </remarks>
        public Contact CreateContact()
        {
            return CreateContact(ContactType.Contact);
        }

        /// <summary>
        /// Create a new Contact associated with this manager.
        /// </summary>
        /// <param name="type">The type of contact to create.  Must be a valid enumeration value.</param>
        /// <returns>Returns a new Contact associated with this manager.</returns>
        /// <remarks>
        /// This is similar to creating a contact with Contact's constructor, except that CommitChanges
        /// will work despite that there is no file initially backing the contact.  After CommitChanges is
        /// called on the contact can later be loaded by Id with this manager.<para/>
        /// The contact is not considered to be part of this manager until it has been committed.
        /// </remarks>
        public Contact CreateContact(ContactType type)
        {
            return new Contact(this, type);
        }

        /// <summary>
        /// Add a Contact to this manager.
        /// </summary>
        /// <param name="contact">The contact to import.</param>
        /// <remarks>
        /// Add a contact that doesn't exist as part of this manager to its collection.
        /// </remarks>
        public void AddContact(Contact contact)
        {
            Validate.NotNull(contact, "contact");
            Contact.CopyToDirectory(contact, RootDirectory);
        }
        
        /// <summary>
        /// Remove a contact with the Id from this manager's collection.
        /// </summary>
        /// <param name="contactId">The id of the contact to remove.</param>
        /// <returns>
        /// Returns whether the manager's collection was modified as a result of this call.
        /// </returns>
        /// <remarks>
        /// Note that even though a contact has been removed the id may still resolve to a contact if called
        /// with ContactManager.Load (e.g. the file has been copied and pasted).
        /// </remarks>
        /// <exception cref="System.IO.IOException">
        /// This will throw if the file that backs the contact can't be deleted because of access restrictions.
        /// </exception>
        public bool Remove(string contactId)
        {
            Validate.NotNull(contactId, "contactId");

            Contact item;
            if (!TryGetContact(contactId, out item))
            {
                // Contact doesn't appear to be from this manager
                return false;
            }

            try
            {
                string path = item.Path;
                Assert.IsNotNull(path);
                if (File.Exists(path))
                {
                    // If the file is locked then it's a legitimate exception to throw here.
                    // MSDN says this doesn't throw if the file doesn't exist.
                    File.Delete(path);
                    return true;
                }
                return false;
            }
            finally
            {
                Utility.SafeDispose(ref item);
            }
        }

        /// <summary>
        /// Get a collection of all the contacts in this manager.
        /// </summary>
        /// <returns>
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public IEnumerable<Contact> GetContactCollection()
        {
            // DirectoryInfo.GetDirectories/GetFiles doesn't work here.
            // For one, the recursive version of GetDirectories doesn't check for reparse points,
            //     so it can end up in an infinite loop.
            // Secondly, GetFiles goes through and returns the full list, and since this enumerator
            //     is likely to bail early that's unnecessarily expensive.
            // So instead this is done mostly manually, and FindFirstFile/FindNextFile are P/Invoked.
            _Validate();

            foreach(FileInfo file in FileWalker.GetFiles(new DirectoryInfo(_rootDirectory), "*.contact"))
            {
                Contact contact;
                if (_TryLoadContactFromFile(file.FullName, out contact))
                {
                    yield return contact;
                }
            }
        }

        /// <summary>
        /// Get or set the contact that represents the user.
        /// </summary>
        /// <exception cref="UnreachableContactException">
        /// The Me contact can't be set to a contact that is not reachable by this ContactManager.
        /// This include contacts that are backed by files outside the user's address book.
        /// </exception>
        public Contact MeContact
        {
            get
            {
                _Validate();

                string id = _GetMeContactId();
                if (string.IsNullOrEmpty(id))
                {
                    return null;
                }

                Contact contact;
                // TryLoad will swallow reasonable exceptions (ERROR_NO_MATCH).
                bool loaded = TryGetContact(id, out contact);
                Assert.Implies(!loaded, null == contact);

                return contact;
            }
            set
            {
                _Validate();
                Validate.NotNull(value, "value");

                // It's generally a logical error to set Me to a contact that can't be loaded (i.e. not committed to disk)
                if (!_IsContainedPath(value.Path))
                {
                    throw new UnreachableContactException("The contact can't be loaded by this manager.");
                }
                _SetMeContactId(value.Id);
            }
        }

        /// <summary>
        /// Get the user's Contacts folder.
        /// </summary>
        /// <remarks>
        /// The ContactManager considers all Contact files in and under this folder to be part of the user's
        /// address book.
        /// </remarks>
        public string RootDirectory
        {
            get
            {
                return _rootDirectory;
            }
        }
        
        public SimpleContactView GetSimpleContact(Contact contact)
        {
            return new SimpleContactView(contact);
        }

        public SimpleContactView GetSimpleContact(string contactId)
        {
            Contact contact = GetContact(contactId);
            return new SimpleContactView(contact);
        }

        public SimpleContactView CreateSimpleContact()
        {
            return new SimpleContactView(CreateContact());
        }

        public SimpleContactView CreateSimpleContact(ContactType type)
        {
            return new SimpleContactView(CreateContact(type));
        }

        public void ImportFromVCard(string filePath)
        {
            TextReader rdr = new StreamReader(filePath);
            ICollection<Contact> contacts = Contact.CreateFromVCard(rdr);

            foreach (Contact c in contacts)
            {
                AddContact(c);
            }
        }

        public IEnumerable<SimpleContactView> GetSimpleContactCollection()
        {
            foreach (Contact contact in GetContactCollection())
            {
                yield return new SimpleContactView(contact);
            }
        }

        public void ImportContactFromDatabase(Guid id)
        {
            SimpleContactDataProvider prvdr = new SimpleContactDataProvider();
            SimpleContactDataSet ds = prvdr.LoadByID(id);

            List<SimpleContactView> simpleContacts = (List<SimpleContactView>)SimpleContactView.CreateFromDataset(ds);

            foreach (SimpleContactView contact in simpleContacts)
            {
                contact.CommitChanges();
            }
        }

        #endregion

        #region IDisposable Pattern

        /// <summary>
        /// Release all resources held by this object.
        /// </summary>
        /// <remarks>
        /// Dispose can safely be called multiple times, but the object cannot be used for anything else.
        /// </remarks>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>Finalize the object.</summary>
        ~ContactManager()
        {
            Dispose(false);
        }

        /// <summary>
        /// Protected overload of Dispose that is standard in IDisposable patterns.
        /// </summary>
        /// <param name="disposing">Whether or not this is being called by Dispose, rather than by the finalizer.</param>
        /// <remarks>
        /// Overrides of this method should always call base.Dispose.
        /// </remarks>
        protected virtual void Dispose(bool disposing)
        {
            _disposed = true;
            if (disposing)
            {
                // This causes the tests to hang ... ? Not sure why.  Should re-enable this.
                //Utility.SafeDispose(ref _watcher);
            }
        }

        #endregion

        #region INotifyCollectionChanged Members

        private event ContactCollectionChangedEventHandler _CollectionChangedInternal;

        /// <summary>
        /// This event gets raised when there has been a change to the collection of contacts.
        /// </summary>
        public event ContactCollectionChangedEventHandler CollectionChanged
        {
            add
            { 
                // The act of subscribing to the event shouldn't really require thread affinity.
                if (null == _watcher)
                {
                    _watcher = new ContactWatcher(this);
                }
                _CollectionChangedInternal += value;
            }
            remove
            {
                _CollectionChangedInternal -= value;
            }
        }

        #endregion
    }
}
